﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MCWorldLibrary.NBT
{
    public enum TagType : byte
    {
        End,
        Byte,
        Short,
        Int,
        Long,
        Float,
        Double,
        ByteArray,
        String,
        List,
        Compound,
        IntArray
    }

    public class Tag
    {
        public TagType Type { get; set; }
        public string Name { get; set; }
        public int Length { get; set; }
        public TagPayload Payload { get; set; }
        public Tag Parent { get; set; }

        public static Tag Deserialize(System.IO.Stream stream)
        {
            return Deserialize(new BigEndianBinaryReader(stream));
        }

        private static Tag Deserialize(BigEndianBinaryReader binaryReader)
        {
            Tag tag = new Tag();
            tag.Type = (TagType)binaryReader.ReadByte();

            if (tag.Type == TagType.End) // END tags has no payload whatsoever
                return tag;

            short nameLength = binaryReader.ReadInt16();
            tag.Name = Encoding.UTF8.GetString(binaryReader.ReadBytes(nameLength));

            tag.Payload = ParsePayload(binaryReader, tag.Type);


            return tag;
        }

        private static TagPayload ParsePayload(BigEndianBinaryReader binaryReader, TagType type)
        {
            TagPayload result = null;
            switch (type)
            {
                case TagType.Byte:
                    result = new BytePayload(binaryReader.ReadByte());
                    break;
                case TagType.Short:
                    result = new ShortPayload(binaryReader.ReadInt16());
                    break;
                case TagType.Int:
                    result = new IntPayload(binaryReader.ReadInt32());
                    break;
                case TagType.Long:
                    result = new LongPayload(binaryReader.ReadInt64());
                    break;
                case TagType.Float:
                    result = new FloatPayload(binaryReader.ReadSingle());
                    break;
                case TagType.Double:
                    result = new DoublePayload(binaryReader.ReadDouble());
                    break;
                case TagType.ByteArray:
                    int byteArraySize = binaryReader.ReadInt32();
                    result = new ByteArrayPayload(binaryReader.ReadBytes(byteArraySize));
                    break;
                case TagType.String:
                    short stringLength = binaryReader.ReadInt16();
                    result = new StringPayload(Encoding.UTF8.GetString(binaryReader.ReadBytes(stringLength)));
                    break;
                case TagType.List:
                    List<TagPayload> payloads = new List<TagPayload>();

                    TagType listType = (TagType)binaryReader.ReadByte();
                    int listSize = binaryReader.ReadInt32();

                    for (int i = 0; i < listSize; ++i)
                    {
                        payloads.Add(ParsePayload(binaryReader, listType));
                    }

                    result = new ListTagPayload(payloads, listType);
                    break;
                case TagType.Compound:
                    List<Tag> children = new List<Tag>();
                    Tag child;
                    while ((child = Tag.Deserialize(binaryReader)).Type != TagType.End)
                    {
                        children.Add(child);
                    }
                    result = new CompoundTagPayload(children);

                    break;
                case TagType.IntArray:
                    int intArraySize = binaryReader.ReadInt32();
                    int[] integers = new int[intArraySize];

                    for (int i = 0; i < intArraySize; ++i)
                        integers[i] = binaryReader.ReadInt32();
                    
                    result = new IntArrayPayload(integers);
                    break;
                default:
                    throw new Exception("Unknown Tag type.");
            }

            return result;
        }

        public void Serialize(System.IO.Stream stream)
        {
            BigEndianBinaryWriter writer = new BigEndianBinaryWriter(stream);
            Serialize(writer);
        }

        private void Serialize(BigEndianBinaryWriter binaryWriter)
        {
            binaryWriter.Write((byte)this.Type);

            if(this.Type == TagType.End)
                return;

            byte[] nameBytes = Encoding.UTF8.GetBytes(this.Name);

            binaryWriter.Write((short)nameBytes.Length);
            binaryWriter.Write(nameBytes);

            SerializePayload(binaryWriter, this.Type, this.Payload);
        }

        private void SerializePayload(BigEndianBinaryWriter binaryWriter, TagType type, TagPayload payload)
        {
            switch (type)
            {
                case TagType.Byte:
                    binaryWriter.Write((payload as BytePayload).Value);
                    break;
                case TagType.Short:
                    binaryWriter.Write((payload as ShortPayload).Value);
                    break;
                case TagType.Int:
                    binaryWriter.Write((payload as IntPayload).Value);
                    break;
                case TagType.Long:
                    binaryWriter.Write((payload as LongPayload).Value);
                    break;
                case TagType.Float:
                    binaryWriter.Write((payload as FloatPayload).Value);
                    break;
                case TagType.Double:
                    binaryWriter.Write((payload as DoublePayload).Value);
                    break;
                case TagType.ByteArray:
                    ByteArrayPayload byteArrayPayload = (ByteArrayPayload)payload;

                    binaryWriter.Write(byteArrayPayload.Value.Length);
                    binaryWriter.Write(byteArrayPayload.Value);
                    break;
                case TagType.String:
                    StringPayload stringPayload = (StringPayload)payload;

                    byte[] stringBytes = Encoding.UTF8.GetBytes(stringPayload.Value);
                    binaryWriter.Write((short)stringBytes.Length);
                    binaryWriter.Write(stringBytes);
                    break;
                case TagType.List:
                    ListTagPayload listPayload = (ListTagPayload)payload;

                    binaryWriter.Write((byte)listPayload.Type);
                    binaryWriter.Write(listPayload.Value.Count);

                    for (int i = 0; i < listPayload.Value.Count; ++i)
                    {
                        SerializePayload(binaryWriter, listPayload.Type, listPayload.Value[i]);
                    }
                    break;
                case TagType.Compound:
                    CompoundTagPayload compoundPayload = (CompoundTagPayload)payload;

                    for (int i = 0; i < compoundPayload.Value.Count; ++i)
                        compoundPayload.Value[i].Serialize(binaryWriter);

                    binaryWriter.Write((byte)TagType.End);

                    break;
                case TagType.IntArray:
                    IntArrayPayload intArrayPayload = (IntArrayPayload)payload;
                    binaryWriter.Write(intArrayPayload.Value.Length);

                    for (int i = 0; i < intArrayPayload.Value.Length; ++i)
                        binaryWriter.Write(intArrayPayload.Value[i]);

                    break;
                default:
                    throw new Exception("Unknown Tag type.");
            }
        }
    }
}
